在 Windows 上编写 GUI 应用程序很痛苦
最近几天,我一直在寻找一个库,可以让我用 C++ 编写带有图形用户界面 (GUI) 的程序。我的要求很简单:
- 只需要支持 Windows
- 允许商业使用
- 容易进行样式定制,包括支持暗黑模式
- 能够生成一个单独的 .exe 文件,没有第三方依赖项,文件体积小于 40MB 以内
- 编写 GUI 部分的时间不应超过实现功能的时间
WinUI 3
刚开始看,WinUI 3 似乎是个不错的选择。它允许使用现代的 Windows 组件,同时还能自定义样式颜色。设计上,你可以使用非常容易掌握的 XAML,或者直接使用 Visual Studio 设计器。
问题:未打包形式的应用程序支持不好。多数情况下,当我尝试将应用程序移动到虚拟机(VM)或不同的电脑上时,由于缺少一些不明确的依赖项,应用程序无法启动。更糟的是,你需要提供一堆处理 WinUI 功能的 .dll 文件。无法生成一个单独的便携式 .exe 文件。打包形式通常没有问题,但它们以 AppX 包安装,这本身带来了很多问题(特别是当你需要访问所有 Win32 API 时)。
Win32 / MFC / 包装 Win32 的小型库
我需要高便携性,因此使用操作系统的原生渲染是合适的。这样程序可以是一个单独的 .exe 文件(假设我们静态链接 MFC),并且会非常小(只有几千字节)。我也可以使用别人已经编写的一个更小的库,这意味着从概念到工作应用会非常快速。
问题:为原生 Win32 控件设置样式非常困难。这需要我为每个控件编写自定义绘制函数,这将花费大量时间,我在这期间都可以养活一个家庭了。有一个用于 Win32 控件的“隐藏”暗模式,Windows 文件资源管理器使用了它,但它只覆盖了一些控件,效果仍然不好看。
Qt
Qt 库被誉为 C++ GUI 的圣杯。尽管它非常复杂,但通过 Qt 样式表 (Qt Style Sheets),你可以轻松进行样式化,其语法类似于 CSS。
问题:如果使用动态链接,运行应用程序需要大量不同的 .dll 文件,总大小超过 40MB。你可以选择将 Qt 静态链接到你的程序中,这将大幅减少文件大小(因为未使用的部分会被移除),但由于 Qt 的 LGPL 许可证,你必须要么将程序开源,要么分发可重新编译的目标文件。或者,你可以花费几千美元购买商业许可证。
wxWidgets
这是一个相当易学的库,并且可以使用 wxFormBuilder。它的许可证比 Qt 更宽松,并且可以静态链接到一个只有 3MB 的可执行文件中。
问题:在 Windows 上,这个库使用的是原生 Win32 组件,并且不提供样式化选项(因为我们不能轻松覆盖绘制函数,甚至比直接使用 Win32/MFC 更糟糕)。虽然它支持应用 Windows 文件资源管理器的暗模式控件,但效果仍然不尽如人意。
hikogui
这是一个相当新的保持模式 (retained mode) GUI 库,使用 Vulkan 作为后端。内置暗模式,并且你可以很容易地自行进行样式化。
问题:要成功编译它,你可能需要计算机科学领域的博士学位,专攻编译器开发。在尝试编译示例超过 30 分钟(包括不同的分支和发布标签)后,我唯一得到的是一个立即崩溃并在某些 Vulkan 库中产生访问冲突的可执行文件,所以我放弃了。尽管我不太喜欢大量使用复杂的 STL(有时甚至没有必要),但它看起来确实很有前途。
Sciter
Sciter 实际上是一个不错的 Electron 替代品,允许你使用 HTML/CSS 来编写桌面应用的 GUI。
问题:你可能会认为 Sciter 的问题在于应用程序大小,但实际上,包含所有 .dll 文件的最终应用程序大小大约为 25MB,这对我来说完全可以接受。如果它是开源的,并且允许商业使用静态链接版本,那就更好了(这和 Qt 面临的问题相同)。不过,由于 Sciter 的价格比 Qt 便宜(目前独立开发者许可证为 310 美元),我愿意支付这笔费用并感到满意。问题是,如图所示(看看标题栏图标),渲染效果不是很好。我遇到了各种字体和图像的抗锯齿问题。而且,无论你做什么,窗口总会有一个相当厚(2-3px)的灰色边框,无法自定义或修改。
WinForms / WPF
如果你在论坛上询问有关 Windows 上 C++ GUI 库的问题,大多数人会告诉你这是个坏主意(我不反对这个看法),建议你使用其他技术栈编写程序的前端部分,然后将 C++ 编写的功能作为组件或模块加载。这可以让你轻松进行样式定制,并显著加快开发速度。技术上可以使用 WinForms/WPF 生成一个小尺寸的单一 .exe 文件。我们可以有两种方法:
- 将 .dll 文件作为资源捆绑到应用程序中,并在运行时提取到某个临时文件夹,然后使用 P/Invoke 从 C#/.NET 应用程序中调用已编译的 .dll 文件。
- 使用 C++/CLI。
问题:.NET 框架在 Windows 10 及更高版本上预装,所以从技术上讲,我们依然符合无依赖项的标准。但捆绑 .dll 文件仍然意味着它需要被提取到某个地方,并且需要编写额外代码使 P/Invoke 正常工作,而 C++/CLI 会编译成 .NET IL 代码,也就是说,你可以在 dnSpy 中打开生成的应用程序,看到 C++ 代码被翻译成等效的 C# 代码(这不是我想要的,我想要的是原生代码)。
解决方案?
以上只是我考虑的几个选项。在尝试了各种不同的库,并在某个时候甚至编写了自己的 MFC 样式之后,我发现对于简单应用程序来说,没有什么比 Dear ImGui 更适合的了。
它有一些缺点,主要是在设计复杂 UI 时,并且它不是保持模式 (retained mode) UI,而是即时模式 (immediate mode) UI,所以我们必须运行一个 GPU 渲染器,比如 DirectX,来渲染每秒 60 帧或更多的帧数,仅用于 UI。
不过,它符合其他所有要求,因为在现代 Windows 版本中默认包含 DirectX。
我写了一个示例,如上图所示,展示了如何使用内置的多视口功能来制作简单的 GUI 应用程序。
编译后的程序大小只有 500KB,并且不需要安装任何额外的东西,即使是 VC++ 可再发行组件也不需要,如果你将 MFC 静态链接进去。